Reading query and form fields in API calls
What you'll see
A developer — or more often an AI code-generation tool — writes an API endpoint as if it were running under Express or Node.js:
// #/API/hello.js — WRONG, all of it
export default function (req, res) {
var name = req.query.name; // TypeError — req is null
var body = JSON.parse(request.body); // request has no body property
res.json({ message: "Hello " + name }); // there is no res — return the value instead
}None of this works. There is no req/res pair in Docly. A parameter named req is looked up as an input field called "req" — which doesn't exist — so it is null, and the first property access throws. The global request object exists, but it holds request metadata (URL, Jwt, file/folder info) — not the query string and not the body.
What's actually happening
When a request hits an #/API/ endpoint, Docly prepares the input before your function runs:
- query (global) — every URL query-string parameter as a key/value map. All values are strings, always.
?page=2givesquery.page === "2", so parse numbers yourself. - form (global) — the request body:
- For form posts (
application/x-www-form-urlencodedormultipart/form-data), each form field becomes a string value:form.email,form.message, … - For JSON posts (
Content-Type: application/json), Docly deserializes the entire body intoform. Values keep their JSON types — nested objects, arrays, numbers and booleans come through as-is.
- For form posts (
- Auto-bound parameters — if the file exports a default (or anonymous) function, each parameter is bound by name from the input:
formis checked first, thenquery.export default function (email, page) { ... }receivesform.email/query.emailandform.page/query.pageautomatically — whichever is present.
The same endpoint serves both GET and POST: on a GET there is simply no body, so form is empty and everything arrives in query.
Two details worth knowing:
- If both
formandquerycontain the same key, form wins in parameter binding. - A JSON body must be an object at the top level (
{ ... }). Posting a bare array or scalar leavesformnull — wrap the payload:{ "items": [...] }.
What to do
Pick the simplest mechanism that fits, and never parse the body yourself.
Do — named parameters for simple endpoints (works for both GET and POST):
// #/API/greet.js
// GET /API/greet?name=Ada → name = "Ada" (from query)
// POST /API/greet {"name": "Ada"} → name = "Ada" (from JSON body)
export default function (name) {
return { message: "Hello, " + (name || "stranger") + "!" };
}Do — the query global for URL parameters (remember: values are strings):
// #/API/articles.js — GET /API/articles?page=2&tag=news
export default function () {
var page = parseInt(query.page || "1");
var tag = query.tag || null;
return docly.getFiles("~/articles")
.filter(a => !tag || a.Tag == tag)
.slice((page - 1) * 20, page * 20);
}Do — the form global for structured JSON bodies:
// #/API/orders.js
// POST /API/orders with Content-Type: application/json
// body: { "customer": { "name": "Ada", "email": "ada@example.com" }, "lines": [ ... ] }
export default function () {
var customer = form.customer; // nested object, types preserved
var lines = form.lines || []; // array comes through as an array
if (!customer || !isEmail(customer.email)) throw "Invalid customer";
return docly.saveFile("~/orders/" + guid() + ".json", { Content: JSON.stringify(form) });
}Do — call it from the browser with ~/ paths:
fetch('~/API/orders', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ customer: { name: 'Ada', email: 'ada@example.com' }, lines: [] })
}).then(r => r.json()).then(result => console.log(result));Don't:
export default function (req) { ... } // no Express-style request argument exists
var body = JSON.parse(request.body); // request has no body — JSON is already in form
var name = request.query.name; // request has no query — use the query globalFile uploads are the one exception: files posted as multipart/form-data do not appear in form. Use docly.getUploads() to list them and docly.getUploadBase64(fieldName) to read one. Ordinary text fields in the same multipart post still arrive in form.
query and form exist only in #/API/ scripts. Hash pages (.hash) are rendered once and cached, so they cannot read per-request input at all — route dynamic input through an API endpoint instead.